import { ComponentPropsOptions, ExtractPropTypes, SetupContext, defineComponent, getCurrentInstance, provide, inject } from 'vue';
import { ComponentEvent, getComponentEmit, useEvent } from "./useEvent";

export function designComponent<PropsOptions extends Readonly<ComponentPropsOptions>,
  Props extends Readonly<ExtractPropTypes<PropsOptions>>,
  Emits extends { [k: string]: (...args: any[]) => boolean },
  Refer,
  >(
    options: {
      name?: string,
      props?: PropsOptions,

      provideRefer?: boolean,
      emits?: Emits,
      setup: (parameter: { props: Props, event: ComponentEvent<Emits>, setupContext: SetupContext<Emits> }) => {
        refer?: Refer,
        render: () => any
      }
    }) {

  const { setup, provideRefer, emits, ...leftOptions } = options

  return {
    ...defineComponent({
      ...leftOptions,
      emits: getComponentEmit(emits),
      setup(props: Props, setupContext: any) {

        const ctx = getCurrentInstance()!
        const event = useEvent<Emits>(emits!)

        if (!setup) {
          console.error('designComponent: setup is required!')
          return () => null
        }

        const { refer, render } = setup({ props, event, setupContext })
        if (!!refer) {
          const duplicateKey = Object.keys(leftOptions.props || {})
            .find(i => Object.prototype.hasOwnProperty.call(refer as any, i))
          if (!!duplicateKey) {
            console.error(`designComponent: duplicate key ${duplicateKey} in refer`)
          } else {
            Object.assign(ctx.proxy, refer)
          }
        }

        if (provideRefer) {
          if (!leftOptions.name) {
            console.error('designComponent: name is required when provideRefer is true!')
          } else {
            provide(`@@${leftOptions.name}`, refer)
          }
        }

        return render
      },
    } as any),
    use: {
      ref: (refName: string) => {
        const ctx = getCurrentInstance()!
        return {
          get value() {
            return ctx.refs[refName] as Refer | null
          }
        }
      },
      inject: (defaultValue?: any) => {
        return inject(`@@${leftOptions.name}`, defaultValue) as Refer
      }
    }
  }
}